using System;
using System.Collections;
using System.Diagnostics;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Reflection;

using Evaluant.Uss.ObjectContext.DynamicProxy;
using Evaluant.Uss.ObjectContext.Descriptors;

using Evaluant.Uss.Collections;
using Evaluant.Uss.Commands;

namespace Evaluant.Uss.ObjectContext
{
	[Serializable]
	public class ObjectContext : IDisposable
	{

		private Transaction _CurrentTransaction;
		private ObjectService _Factory;
		private ReflectionEntityGetter _EntityGetter;
		private IPersistenceEngine _PersistenceEngine;
        private Hashtable _FieldsInfoCache = new Hashtable();
		private Hashtable _GlobalProcessedObjects = new Hashtable();
		private HashedList _PendingObjects;
		private bool _AllowLoadingReference;

		private const string SWITCH_OPATH = "Evaluant.Uss.OPath";
		private const string SWITCH_OPATH_DESC = "Evaluant.Uss.OPath";
		private const string SWITCH_OPATH_DEFAULT = "0";

		protected BooleanSwitch oPathSwitch = new BooleanSwitch(SWITCH_OPATH, SWITCH_OPATH_DESC);

		/// <summary>
		/// Gets or sets the persistence engine.
		/// </summary>
		/// <value></value>
		public IPersistenceEngine PersistenceEngine
		{
			get { return _PersistenceEngine; }
			set { _PersistenceEngine = value; }
		}

		/// <summary>
		/// Creates a new <see cref="ObjectContext"/> instance.
		/// </summary>
		/// <param name="factory">Creator.</param>
		/// <param name="engine">Engine.</param>
		protected internal ObjectContext(ObjectService factory, IPersistenceEngine engine)
		{
			_Factory = factory;
			_PersistenceEngine = engine;
			_EntityGetter = new ReflectionEntityGetter(factory.PersistentDescriptor, _PersistenceEngine.Model);
			_AllowLoadingReference = true;
		}

		protected ObjectContext()
		{
		}

		/// <summary>
		/// Gets or sets the parent factory.
		/// </summary>
		/// <value></value>
		public ObjectService Factory
		{
			get { return _Factory; }
			set { _Factory = value; }
		}

		/// <summary>
		/// Converts the namespace domain to euss.
		/// </summary>
		/// <param name="ns">The namespace to convert.</param>
		/// <returns></returns>
		public static string ConvertNamespaceDomainToEuss(string ns)
		{
			return ns.Replace(".", ":");
		}

		/// <summary>
		/// Converts the namespace domain to euss and add full entity name if necessary.
		/// </summary>
		/// <param name="ns">The namespace to convert.</param>
		/// <returns></returns>
		private static string ConvertNamespaceDomainToEuss(Type entityType, string opathConstraint)
		{
			// if opath with more than one entity (ie: A[a=1].B[b=2].C), type to load is C, then just add namespace for A.
			// if opath with just one entity (ie: [a=1]), type to load is A, then add namespace with entityName for A.
			// This assumes that all entity have the same namespace
			string firstEntityOrNamespace = (opathConstraint.StartsWith("[") || opathConstraint.StartsWith(".") || opathConstraint.Length == 0) ? entityType.FullName : String.Concat(entityType.Namespace, ".");
			return String.Concat(firstEntityOrNamespace.Replace(".", ":"), opathConstraint);
		}

		/// <summary>
		/// Converts the namespace euss to domain.
		/// </summary>
		/// <param name="ns">The euss namespace to convert.</param>
		/// <returns></returns>
		public static string ConvertNamespaceEussToDomain(string ns)
		{
			return ns.Replace(":", ".");
		}

		/// <summary>
		/// Get Entity of the specified object
		/// </summary>
		/// <param name="object">The object</param>
		/// <returns>the Entity associated to the object</returns>
		public Entity ExtractEntity(object item)
		{
			if (item == null)
				throw new ArgumentNullException("item");

			Hashtable local = new Hashtable();
			
			Entity e =_EntityGetter.Get(item, local, _GlobalProcessedObjects);

            TrackLocallyProcessed(local);

			return e;
		}

		/// <summary>
		/// Updates an Entity with an external list.
		/// </summary>
		/// <remarks>
		/// Only used by dnamic proxies
		/// </remarks>
		/// <param name="parent">The parent.</param>
		/// <param name="role">The role.</param>
		/// <param name="values">The values.</param>
		public void UpdateEntitySet(IPersistableProxy parent, string role, IList values)
		{
			// don't load reference if user is trying to create just one relationship or serializing an object
			if (!_AllowLoadingReference && !parent.Entity.InferredReferences.Contains(role))
				return;

			HashedList list = new HashedList();

			// load entities if not already done
			foreach(object item in values)
				list.Add(ExtractEntity(item));

			EntitySet currentList = parent.Entity.GetEntitySet(role);

			// Detect removed relationships
			foreach(Entity e in currentList)
				if(!list.Contains(e))
					parent.Entity.DeleteElement(role, e);

			// Add new relationships
			foreach(Entity e in list)
				if(!currentList.Contains(e))
					parent.Entity.AddValue(role, e);
		}

		/// <summary>
		/// Serializes the specified persistable object.
		/// </summary>
		/// <param name="persistable">Persistable object.</param>
		public void Serialize(IPersistable persistable)
		{
			if(persistable == null)
				throw new ArgumentNullException("persistable");

			if(persistable.Entity == null)
				throw new NullReferenceException("Entity cannot be null");

			EnsureTransactionCreated();
			
			_CurrentTransaction.Serialize(persistable.Entity);  

			// Adds the Entity to the cache after Serialize for it can throw an exception or change the Id's value
			TrackObject(persistable); 
		}

		/// <summary>
		/// Serializes the specified object.
		/// </summary>
		/// <param name="item">Object to serialize.</param>
		public void Serialize(object item)
		{
			if(item == null)
				throw new ArgumentNullException("item");

			EnsureTransactionCreated();
			
			
			// Stores the object in a temporary list to extract its entity during CommitTransaction
			if (!_PendingObjects.Contains(item))
				_PendingObjects.Add(item);

			IPersistable persistable = item as IPersistable;

			if(persistable != null)
			{
				// Adds the Entity to the cache after Serialize for it can throw an exception or change the Id's value
				TrackObject(persistable); 
			}
		}

		/// <summary>
		/// Serializes the specified objects.
		/// </summary>
		/// <param name="items">Objects to serialize.</param>
		public void Serialize(IEnumerable items)
		{
			foreach(object item in items)
				Serialize(item);
		}

		/// <summary>
		/// Deletes the specified object.
		/// </summary>
		/// <param name="persistable">Persistable object.</param>
		public void Delete(object o)
		{
			if(o == null)
				throw new ArgumentNullException("o");

			IPersistable persistable = o as IPersistable;

			if(persistable == null && !_GlobalProcessedObjects.Contains(o))
				throw new ArgumentException("This object is not persisted.");

			Entity e = persistable == null ? (Entity)_GlobalProcessedObjects[o] : persistable.Entity ;

			EnsureTransactionCreated();

			UnTrackObject(e.Type, e.Id);
			_CurrentTransaction.Delete(e);    
		}

		/// <summary>
		/// Serializes the specified persistable objects.
		/// </summary>
		/// <param name="persistables">Persistable objects.</param>
		public void Serialize(IPersistableCollection persistables)
		{
			if(persistables == null)
				throw new ArgumentNullException("persistables");

			foreach(IPersistable persistable in persistables)
				Serialize(persistable);    
		}

		/// <summary>
		/// Serializes the specified persistable objects.
		/// </summary>
		/// <param name="persistables">Persistable objects.</param>
		public void Serialize(IPersistable[] persistables)
		{
			if(persistables == null)
				throw new ArgumentNullException("persistables");

			foreach(IPersistable persistable in persistables)
				Serialize(persistable);    
		}

		/// <summary>
		/// Deletes the specified persistable objects.
		/// </summary>
		/// <param name="persistables">Persistable objects.</param>
		public void Delete(IPersistableCollection persistables)
		{
			if(persistables == null)
				throw new ArgumentNullException("persistables");

			foreach(IPersistable persistable in persistables)	
				Delete(persistable);    	
		}

		/// <summary>
		/// Deletes the specified persistable objects.
		/// </summary>
		/// <param name="persistables">Persistable objects.</param>
		public void Delete(IPersistable[] persistables)
		{
			if(persistables == null)
				throw new ArgumentNullException("persistables");

			foreach(IPersistable persistable in persistables)	
				Delete(persistable);    	
		}
		
		/// <summary>
		/// Disposes this instance.
		/// </summary>
		public void Dispose()
		{
			Clear();		
		}
		
		/// <summary>
		/// Initializes the repository.
		/// </summary>
		public void InitializeRepository()
		{
			_PersistenceEngine.InitializeRepository();

      _GlobalProcessedObjects.Clear();
    
			// Empties the cache
			Clear();
		}

		/// <summary>
		/// Removes all IPersistable from the cache
		/// </summary>
		/// <remarks>
		/// All removes IPersistable are disconnected from the ObjectContext.
		/// </remarks>
		public void Clear()
		{
			foreach(IdentityMap im in _TypeTable.Values)
			{
				im.Clear();
			}

			_TypeTable.Clear();
		}
 
		// Contains IdentityMap instances indexed by the Type of the objects they contain
		private Hashtable _TypeTable = new Hashtable();

		/// <summary>
		/// Removes an object from the first-level cache.
		/// </summary>
		/// <param name="persistable">Persistable object.</param>
		public void RemoveFromCache(IPersistable persistable)
		{
			if(persistable == null)
				throw new ArgumentNullException("persistable");

			IdentityMap im = _TypeTable[persistable.Entity.Type] as IdentityMap;

			if(im != null)
			{
				persistable.ObjectContext = null;
				im.Remove(persistable.Entity.Id);
			}
		}

		/// <summary>
		/// Returns a object object wrapping a given Entity from the internal cache or creates a new one
		/// </summary>
		/// <param name="entity">The entity to wrap</param>
		/// <returns>The object object wrapping an entity</returns>
		protected object GetInstance(Entity entity)
		{

			// Search this Entity in the cache
			object tracked = GetTrackedObject(entity.Type, entity.Id);

			// If the entity has not yet been cached, create a wrapper on it and cache it
			if (tracked == null)
			{
				IPersistable persistable;

				// Search for the business type corresponding to this entity
				Type type = _Factory.GetDomainType(entity.Type);

				// If no type was found, throw an exception
				if(type == null)
					throw new UniversalStorageException("Type not found: " + ConvertNamespaceEussToDomain(entity.Type) + "\nPlease use AddAssembly() to resolve the type's location");

				// Gets a dynamic proxy instance if the class doesn't implement IPersistable directly
				//if(type.IsAssignableFrom(typeof(IPersistable)))
				if (typeof(IPersistable).IsAssignableFrom(type))
				{
					persistable = (IPersistable)Activator.CreateInstance(type, new object[] { entity }, null);
					tracked = persistable;
				}
				else
				{
					persistable = _Factory.PersistableProxyFactory.CreatePersistableProxy(type);
					persistable.Entity = entity;

					persistable.ObjectContext = this;
					((IPersistableProxy)persistable).Set();

					tracked = persistable;
				}

				// Add the newly created business object to the cache
				TrackObject(persistable);
			}

			// The item is already into the cache or has been created
			return tracked;
		}


		public IList LoadWithEntities(EntitySet entities)
		{
			// If no entity was found, return an empty array
			if(entities.Count == 0)
				return new IPersistableCollection();

			// Create an empty array (initialized with the number of found entities)
			HashedList result = new HashedList();

			// Use a for loop as the Enumerator looses the order
			// Use a for loop as the Enumerator looses the order
			for (int i=0; i<entities.Count; i++)
			{
				Entity entity = entities[i];

				// Gets a wrapper over this entity (a new one or from the cache)
				object persistable = GetInstance(entity);

				// If nothing went wrong add it to the result array
				if (persistable != null)
					result.Add(persistable);
			}

			// Give back a strongly typed collection
			return result;
		}

		/// <summary>
		/// Load a collection of objects with a <see cref="Query"/> object
		/// </summary>
		/// <param name="query">the query</param>
		/// <returns>A collection of object matching the query</returns>
		public IList Load(Query query)
		{
			_Factory.AddAssembly(query.FromType.Assembly);
			
			string[] attributes = new string[query.InferredAttributes.Count];
			query.InferredAttributes.CopyTo(attributes, 0);

			string[] orderbies = new string[query.OrderBy.Count];
			query.OrderBy.CopyTo(orderbies, 0);
			string orderby = String.Join(",", orderbies);

			string opath = ObjectContext.ConvertNamespaceDomainToEuss(query.FromType, query.Constraint);

			if (oPathSwitch.Enabled)
			{
				System.Diagnostics.Trace.WriteLine(opath);
			}

			EntitySet result = _PersistenceEngine.Load(opath, attributes, orderby, query.FirstResult, query.MaxResults);

			if(query.InferredRelationships.Count > 0 && result.Count > 0)
			{
				string[] references = new string[query.InferredRelationships.Count];
				query.InferredRelationships.CopyTo(references, 0);
				
				_PersistenceEngine.LoadReference(result, references);

			}

			return LoadWithEntities(result);
		}

		/// <summary>
		/// Load all objects of the specified type 
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns>A collection of objects</returns>
		public IList Load(Type type)
		{
			return Load(type, String.Empty);
		}

		/// <summary>
		/// Load all objects of the specified type, matching the constraint
		/// </summary>
		/// <param name="type">The type.</param>
		/// <param name="constraint">The constraint (opath syntax)</param>
		/// <returns>A collection of objects</returns>
		public IList Load(Type type, string constraint)
		{
			_Factory.AddAssembly(type.Assembly);

			string opath = ObjectContext.ConvertNamespaceDomainToEuss(type, constraint);

			if (oPathSwitch.Enabled)
			{
				System.Diagnostics.Trace.WriteLine(opath);
			}

			return LoadWithEntities(_PersistenceEngine.Load(opath));
		}

		/// <summary>
		///Loads an object from an existing id
		/// </summary>
		/// <param name="type">The type of the object to retrieve</param>
		/// <param name="id">The id of the object to retrieve</param>
		/// <returns>The object with the corresponding id</returns>
		public object LoadWithId(Type type, string id)
		{
			IList result = LoadWithId(type, new string[] { id });
			return result.Count > 0 ? result[0] : null;
		}

		/// <summary>
		/// Loads some objects from an existing id
		/// </summary>
		/// <param name="type">The type of the object to retrieve</param>
		/// <param name="id">The id of the object to retrieve</param>
		/// <returns>The object with the corresponding id</returns>
		/// <remarks>The exact type of the entity to find must be specified, not a base type. It must be so as entities of different types can have the same id.</remarks>
		public IList LoadWithId(Type type, string[] id)
		{
			_Factory.AddAssembly(type.Assembly);

			// Load the entities corresponding to the id
			EntitySet entities = _PersistenceEngine.LoadWithId(ObjectContext.ConvertNamespaceDomainToEuss(type.FullName), id);
			return LoadWithEntities(entities);
		}

		/// <summary>
		/// Loads only one object object using a <cref>Query</cref> instance
		/// If more than one objects match the query, return the first.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns>the object found</returns>
		public object LoadSingle(Type type)
		{
			return LoadSingle(type, String.Empty);
		}

		/// <summary>
		/// Loads only one object object using a <cref>Query</cref> instance
		/// If more than one objects match the query, return the first.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <param name="constraint">criterias that object must match (opath syntax)</param>
		/// <returns>the object found</returns>
		public object LoadSingle(Type type, string constraint)
		{
			Query query = new Query(type, constraint);
			query.MaxResults = 1;
			IList result = Load(query);
			return result.Count > 0 ? result[0] : null;
		}

		/// <summary>
		/// Compute an arithmetic opath expression 
		/// </summary>
		/// <param name="opath">the opath, must start with one of these functions (count(), avg(), sum(), min(), max())</param>
		/// <returns>the result of the expression</returns>
		public object LoadScalar(string opath)
		{
			return _PersistenceEngine.LoadScalar(opath);
		}


		/// <summary>
		/// Loads all the referenced objects for a given entity and a role name. This is the Lazy Loading logic.
		/// </summary>
		/// <param name="entity">The Entity object from which the referenced items have to be loaded</param>
		/// <param name="name">The name of the role to load the linked entities from</param>
		/// <returns>The collection of the loaded objects</returns>
		public IList LoadReference(Entity entity, string name)
		{
			// Use the underlying Persistence Engine to Lazy Load the reference
			Entity container = new Entity(entity.Type);
			container.Id = entity.Id;
			_PersistenceEngine.LoadReference(new Entity[] { container }, new string[] { name });

			// Mark reference as "loaded"
			if (!entity.InferredReferences.Contains(name))
				entity.InferredReferences.Add(name);

			// If no entity was found, return an empty array
			if(container.MemberCount == 0)
				return new HashedList();

			// Create an empty array (initialized with the number of found entities)
			HashedList result = new HashedList();

			State state = entity.State;
			foreach(EntityEntry entry in container)
			{
				Entity e = (Entity)entry.Value;

				// Search this entity into the cache or create a new wrapper
                object instance = GetInstance(e);

                // Add the cached entry
                IPersistable persistable = instance as IPersistable;

                if (persistable != null)
                {
                    entity.AddValue(name, persistable.Entity, typeof(Entity), State.UpToDate);
                }

                // If nothing went wrong add it to the result array
                result.Add(instance);
            }
			entity.State = state;

			// Give back a strongly typed collection
			return result;
		}
		
		/// <summary>
		/// Loads the parent objects for a given relationship
		/// </summary>
		/// <param name="parentType">The type of the object you want to retreive</param>
		/// <param name="fromObject">The serialized child object whose parent must be loaded</param>
		/// <param name="role">The role between the parent and the child object</param>
		/// <returns></returns>
		public IList LoadParents(Type parentType, object fromObject, string role)
		{
            string id = String.Empty;

            IPersistable persistable = fromObject as IPersistable;

            if (persistable == null)
            {
                if (_GlobalProcessedObjects.ContainsKey(fromObject))
                {
                    id = ((Entity)_GlobalProcessedObjects[fromObject]).Id;
                }
                else
                {
                    throw new ArgumentException("The argument is either null or is not a dynamic proxy", "fromObject");
                }
            }
            else
            {
                id = persistable.Entity.Id;
            }

			return Load(parentType, String.Concat("[exists(", role, "[id('", id, "')])]"));
		}

        public void TrackObject(Entity entity, object o)
        {
            // Can be null if it is initialized in the constructor and TrackChildren is called in a dynamic proxy
            if (o == null)
                return;

            if (!_TypeTable.ContainsKey(entity.Type))
                _TypeTable.Add(entity.Type, new IdentityMap());

            if (!_GlobalProcessedObjects.ContainsKey(o))
                _GlobalProcessedObjects.Add(o, entity);

            IdentityMap im = (IdentityMap)_TypeTable[entity.Type];
            im.Add(entity.Id, o);
        }

		/// <summary>
		/// Adds an IPersistable object to the first-level cache
		/// </summary>
		/// <param name="o">The object to add to the cache</param>
		public void TrackObject(IPersistable o)
		{
			// Can be null if it is initialized in the constructor and TrackChildren is called in a dynamic proxy
			if (o == null)
				return;

			if( !_TypeTable.Contains(o.Entity.Type))
				_TypeTable.Add(o.Entity.Type, new IdentityMap());

			IdentityMap im = _TypeTable[o.Entity.Type] as IdentityMap ;

			if (im.Add(o))
			{
				o.ObjectContext = this; 
				o.TrackChildren();
			}
		}

		/// <summary>
		/// Adds a collection of IPersistable to the cache
		/// </summary>
		/// <param name="pc">The objects to add to the cache</param>
		public void TrackObject(IPersistableCollection pc)
		{
			if(pc == null)
				return;

			pc.ObjectContext = this;

			if(pc.IsLoaded())
                foreach (object p in pc)
                {
                    if(p is IPersistable)
                        TrackObject((IPersistable)p);
                }
		}

		/// <summary>
		/// Adds an array of IPersistable to the cache
		/// </summary>
		/// <param name="pc">The objects to add to the cache</param>
		public void TrackObject(IPersistable[] pc)
		{
			if(pc == null)
				return;

			foreach(IPersistable p in pc)
				TrackObject(p);
		}

		/// <summary>
		/// Gets an object from the cache if it exists
		/// </summary>
		/// <param name="type">The type of the tracked entity</param>
		/// <param name="id">The identifier of the tracked entity</param>
		/// <returns>Null if the entity is not tracked</returns>
		public object GetTrackedObject(string type, string id)
		{
			if (_TypeTable.ContainsKey(type))
			{
				IdentityMap im = (IdentityMap)_TypeTable[type];
				return im.Get(id);
			}
			
			return null;
		}

		/// <summary>
		/// Removes an IPersistable from the cache
		/// </summary>
		/// <param name="type">The type of the tracked entity</param>
		/// <param name="id">The identifier of the tracked entity</param>
		protected void UnTrackObject(string type, string id)
		{
			if(_TypeTable.ContainsKey(type))
			{		
				IdentityMap im = (IdentityMap)_TypeTable[type];
				im.Remove(id);
			}	
		}

		/// <summary>
		/// Begins the transaction.
		/// </summary>
		public void BeginTransaction()
		{
			EnsureTransactionDisposed();
			_CurrentTransaction = new Transaction();
			_PendingObjects = new HashedList();
		}

		/// <summary>
		/// Ensures the transaction is created.
		/// </summary>
		private void EnsureTransactionCreated()
		{
			if(_CurrentTransaction == null)
				throw new UniversalStorageException("BeginTransaction() must be called before");
		}

		/// <summary>
		/// Ensures the transaction is disposed.
		/// </summary>
		private void EnsureTransactionDisposed()
		{
			if(_CurrentTransaction != null)
				throw new UniversalStorageException("CommitTransaction() or RollbackTransaction() must be called before");
		}

		private void TrackLocallyProcessed(Hashtable local)
		{
			foreach (DictionaryEntry kvp in local)
			{
				TrackObject((Entity)kvp.Value, kvp.Key);
			}
		}

		/// <summary>
		/// Commits the transaction.
		/// </summary>
		public void CommitTransaction()
		{
			EnsureTransactionCreated();

			_AllowLoadingReference = false;

			try
			{
				foreach (object item in _PendingObjects)
				{
					// Creates a new entity representing this "nude" object

					Hashtable local = new Hashtable();
					Entity e = _EntityGetter.Get(item, local, _GlobalProcessedObjects);
					TrackLocallyProcessed(local);

					_CurrentTransaction.Serialize(e);
				}
			}
			finally
			{
				_AllowLoadingReference = true;
			}

            try
            {
                _CurrentTransaction.Commit(_PersistenceEngine);

                if (_CurrentTransaction.NewIds.Count > 0)
                {
                    foreach (object item in _PendingObjects)
                    {
                        Type targetType = item.GetType();

                        FieldInfo idFieldInfo = null;
                        
                        if (_FieldsInfoCache.Contains(targetType))
                        {
                            idFieldInfo = _FieldsInfoCache[targetType] as FieldInfo;
                        }

                        if (idFieldInfo == null)
                        {

                            PropertyDescriptor idPropertyDesc = _Factory.PersistentDescriptor.GetIdDescriptor(targetType);
                            if (idPropertyDesc != null)
                            {
                                idFieldInfo = targetType.GetField(idPropertyDesc.FieldName,
                                    BindingFlags.NonPublic | BindingFlags.Instance);

                                if(idFieldInfo != null)
                                {
                                    _FieldsInfoCache.Add(targetType, idFieldInfo);
                                }
                            }
                        }

                        if (idFieldInfo != null)
                        {
                            Entity e = _GlobalProcessedObjects[item] as Entity;
                            idFieldInfo.SetValue(item, Convert.ChangeType(e.Id, idFieldInfo.FieldType));
                        }
                    }
                }
            }
            catch (Exception inner)
            {
                throw new UniversalStorageException("An error occured while commiting a transaction", inner);
            }
            finally
            {
                _CurrentTransaction = null;
            }
        }

		/// <summary>
		/// Rolls back the transaction.
		/// </summary>
		public void RollBackTransaction()
		{
			_CurrentTransaction = null;
		}

        private void CreateRelationship(Entity parent, Entity child, string role)
        {
            EnsureTransactionCreated();

            CreateReferenceCommand command = new CreateReferenceCommand(role, parent.Id, parent.Type, child.Id, child.Type);
            _CurrentTransaction.PushCommand(new Command[] { command });
        }

        /// <summary>
        /// Creates a relationship. Be careful, Parent and Child objects must have their Id property initialized !
        /// </summary>
        /// <param name="parent">Parent.</param>
        /// <param name="child">Child.</param>
        /// <param name="role">Role.</param>
        public void CreateRelationship(object parent, object child, string role)
        {
            try
            {
                _AllowLoadingReference = false;     // to be sure not to load unloaded references if not needed

                Hashtable localProcessed = new Hashtable();

                Entity parentEntity = parent is IPersistable
                    ? ((IPersistable)parent).Entity
                    : _EntityGetter.Get(parent, localProcessed, _GlobalProcessedObjects);

                Entity childEntity = child is IPersistable
                    ? ((IPersistable)child).Entity
                    : _EntityGetter.Get(child, localProcessed, _GlobalProcessedObjects);

                TrackLocallyProcessed(localProcessed);

                CreateRelationship(parentEntity, childEntity, role);
            }
            finally
            {
                _AllowLoadingReference = true;
            }
        }

		/// <summary>
		/// Detaches the specified graph.
		/// </summary>
		/// <param name="graph">The graph.</param>
		/// <returns>A cloned object without any relation with a Persistence Engine</returns>
		/// <remarks>Only the loaded relationship are cloned during the process.</remarks>
		public object Detach(object graph)
		{
			// By using BinarySerialization the graph is cloned.
			// Moreover as the ISerializable.GetObjectData() method is defined on each Proxy type,
			// the serialization type is overriden to be replaced by the original type.
			// Thus deserializing the stream generates an original type array.
			// IPersistableCollection is also ISerializable to replace its instances by ArrayList.

			IPersistable persistable = graph as IPersistableProxy;

			if(persistable == null)
				throw new ArgumentException("The argument is either null or is not a dynamic proxy", "graph");

			BinaryFormatter formatter = new BinaryFormatter();
			MemoryStream ms = new MemoryStream();
			formatter.Serialize(ms, persistable);
			ms.Position = 0;
			object obj = formatter.Deserialize(ms);
			ms.Close();

			return obj;
		}

        /// <summary>
        /// Adds a set of Command object to the current transaction
        /// </summary>
        public void ExecuteCommand(ICollection commands)
        {
            EnsureTransactionCreated();

            ArrayList list = new ArrayList(commands);
            _CurrentTransaction.PushCommand((Command[])list.ToArray(typeof(Command)));
        }

        /// <summary>
        /// Adds a Command object to the current transaction
        /// </summary>
        public void ExecuteCommand(Command command)
        {
            EnsureTransactionCreated();
            _CurrentTransaction.PushCommand(new Command[] { command });
        }


        private void RemoveRelationship(Entity parent, Entity child, string role)
        {
            EnsureTransactionCreated();

            DeleteReferenceCommand command = new DeleteReferenceCommand(role, parent.Id, parent.Type, child.Id, child.Type);
            _CurrentTransaction.PushCommand(new Command[] { command });
        }

        /// <summary>
        /// Removes a relationship.
        /// </summary>
        /// <param name="parent">Parent.</param>
        /// <param name="child">Child.</param>
        /// <param name="role">Role.</param>
        public void RemoveRelationship(object parent, object child, string role)
        {
            Hashtable localProcessed = new Hashtable();

            Entity parentEntity = parent is IPersistable
                ? ((IPersistable)parent).Entity
                : _EntityGetter.Get(parent, localProcessed, _GlobalProcessedObjects);

            Entity childEntity = child is IPersistable
                ? ((IPersistable)child).Entity
                : _EntityGetter.Get(child, localProcessed, _GlobalProcessedObjects);

            TrackLocallyProcessed(localProcessed);

            RemoveRelationship(parentEntity, childEntity, role);
        }

        /// <summary>
        /// Determines whether the specified property is null.
        /// </summary>
        /// <param name="item">The entity.</param>
        /// <param name="property">The property.</param>
        /// <returns>
        /// 	<c>true</c> if the specified property is null; otherwise, <c>false</c>.
        /// </returns>
        public bool IsNull(object item, string property)
        {
            IPersistable persistable = item as IPersistable;

            if (persistable == null)
                throw new PersistenceManagerException("You can only use ObjectContext loaded objects");

            return persistable.Entity.IsNull(property);
        }

        /// <summary>
        /// Sets a property to null.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <param name="property">The property.</param>
        public void SetNull(object item, string property)
        {
            IPersistable persistable = item as IPersistable;

            if (persistable == null)
                throw new PersistenceManagerException("You can only use ObjectContext loaded objects");

            persistable.Entity.SetValue(property, null);
        }

        /// <summary>
        /// Gets the id of an entity.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <returns>The identifier of the entity</returns>
        public string GetId(object entity)
        {
            IPersistable persistable = entity as IPersistable;

            if (persistable != null)
            {
                return persistable.Entity.Id;
            }
            else if (_GlobalProcessedObjects.Contains(entity))
            {
                return ((Entity)_GlobalProcessedObjects[entity]).Id;
            }
            else
            {
                throw new PersistenceManagerException("You can only use ObjectContext loaded objects");
            }
        }

        /// <summary>
        /// Sets the id of an entity.
        /// </summary>
        /// <param name="entity">The entity.</param>
        /// <param name="id">The id.</param>
        public void SetId(object entity, string id)
        {
            IPersistable persistable = entity as IPersistable;

            if (persistable != null)
            {
                persistable.Entity.Id = id;
            }
            else if (_GlobalProcessedObjects.Contains(entity))
            {
                ((Entity)_GlobalProcessedObjects[entity]).Id = id;
            }
            else
            {
                throw new PersistenceManagerException("You can only use ObjectContext loaded objects");
            }
        }
        
        /// <summary>
		/// Imports the specified entities and their related.
		/// </summary>
		/// <param name="entities">Entities to import.</param>
		/// <remarks>
		/// Only the related objects which are already loaded will be imported. 
		/// Thus it allows you to import only one node of the object graph.
		/// </remarks>
		public void Import(IList entities)
		{
			foreach(IPersistable entity in entities)
				Import(entity);
		}

		public void Import(IPersistable item)
		{
			if(item == null)
				throw new ArgumentNullException("item");

            // Checks if this objet has already been imported in this context
            if (GetTrackedObject(item.Entity.Type, item.Entity.Id) != null)
                return;

            item.Entity.State = State.New;

			// Registers this item and all related to the current ObjectContext
			TrackObject(item);
			
			foreach(EntityEntry entry in item.Entity)
			{
				entry.State = State.New;
				if(entry.IsEntity)
				{
					Entity related = (Entity)entry.Value;
					IPersistable relatedItem = GetTrackedObject(related.Type, related.Id) as IPersistable;
					if(related != relatedItem.Entity)
						throw new UniversalStorageException("Items should be the same.");

					Import(relatedItem);
				}
			}
		}
	}
}
